#!/bin/bash

#   Bash Plugin Architecture
#
#   Author: Fraser Scott
#   Copyright (C) 2010 Fraser Scott
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

# TODO - Currently a plugin can override an internal function (e.g. array_get).
# This is probably a bad idea. Need to look into ways of detect/preventing this.
# - Fraser Scott (02/07/2010)
# TODO - Check for conflicting plugin names
# - Fraser Scott (02/07/2010)
# TODO - Perhaps work in some form of validation helper functions? E.g. ensuring
# required tools/commands/files/directories are available.
# - Fraser Scott (16/08/2010)
# TODO - What would also be nice is support more than two levels of command. Not
# sure how that would work though.
# - Fraser Scott (17/08/2010)
# TODO - Inheritance :)
# e.g. | inherit Ball from Objects
#      | Ball() {...
# which basically means run Objects before Ball
# - Fraser Scott (13/09/2010)

set -e

################################################################################
# Variables
################################################################################

sBPAVersion="0.4"
sAppName=""
sAppPluginsDir=""

doDebug=0

typeset -a aAppArgs
typeset -a pluginNames
typeset -a helperNames

sSelectedPlugin=""
sSelectedPluginSwitch=""
sSelectedCommand=""

sScriptVersion=""

################################################################################
# Default Plugin functions
################################################################################

Main()
{
    echo "This command doesn't do anything at the moment, try adding --help"
    exit 0
}

sHelpDefault="No detailed help available"
Help()
{
    echo "$sHelpDefault"
}

version=""
Version()
{
	echo $version
}

################################################################################
# Helper functions
################################################################################

#
# Echoes the given message if global variable $doDebug is true.
#
# @param message
#   String containing message to be echoed if debugging is enabled.
#
# @return
#   Does not return a value
#
debug()
{
    if [ "$doDebug" -ne "0" ]; then
        echo "DEBUG $1"
    fi
}

#
# Echoes the given message and exits with given exit value
#
# @param message
#   String containing message to be shown
#
# @param exit
#   Exit value
#
# @return
#   Does not return a value
#
fatal()
{
	msg=${1:-"An unspecified error occured"}
	err=${2:-1}

	echo "FATAL $msg"
	exit $err
}

### Array functions ###

#
# Pushes value on to end of the array.
#
# @param name
#   Name of the array onto which the value will be pushed.
#
# @param value
#   Value to be pushed onto given array.
#
# @return
#   Returns index of pushed value
#
array_push()
{
    name=$1
    value=$2
    eval index=\$\{\#$name\[\@\]\}
    eval $name\[\$index\]=\"$value\"
}

#
# Pops a value from the given array.
#
# @param name
#   Name of the array to pop a value from.
#
# @return
#   The value from the end of the array.
#
array_pop()
{
    name=$1
    eval index=\$\(\(\$\{\#$name\[\@\]\}\-\1\)\)
    eval value=\$\{$name\[\$index\]\}
    eval unset \$\{$name\[\$index\]\}
    echo -n "$value"
}

#
# Returns all entries in the given array.
#
# @param name
#   Name of the array.
#
# @return
#   Bash 'list' of values in the array
#
array_each()
{
    name=$1

    # The follow commented code is overkill for current
    # requirements, but might be needed if one wants to
    # specify a delimiter...

    #eval size=\$\{\#$name\[\@\]\}
    #for ((i=0;i<$size;i++)); do
    #        eval echo \$\{$name\[\$i\]\}
    #done

    eval echo \$\{$name\[\@\]\}
}

#
# Returns the value at a given index in the specified array
#
# @param name
#   Name of the array
#
# @param index
#   Index to return
#
# @return
#   Returns value at given index
#
array_get()
{
    name=$1
    index=$2
    eval value=\$\{$name\[\$index\]\}
    echo -n "$value"
}

#
# Returns the index of the given element in the array
#
# @param name
#   Name of the array
#
# @param value
#   Element value to search for
#
# @return
#   Returns index value for element
#
array_find()
{
    name=$1
    value=$2

    eval size=\$\{\#$name\[\@\]\}
    for ((i=0;i<$size;i++)); do
        eval found=\$\{$name\[\$i\]\}
        if [ "$found" == "$value" ]; then
            echo -n "$i"
            return
        fi
    done
}

#
# Sets the element at the given index
#
# @param name
#   Name of the array
#
# @param index
#   Index to set
#
# @param value
#   Value to set
#
# @return
#   Does not return anything
#
array_set()
{
    name=$1
    index=$2
    value=$3
    eval $name\[\$index\]=\"$value\"
}

#
# Adds an arbitrary number of elements to array
#
# @param name
#   Name of the array
#
# @param ...value...
#   Values to be inserted in the array
#
# @return
#   Does not return a value
#
array_add()
{
    name=$1
    shift
    while (( "$#" )); do
        array_push "$name" "$1"
        shift        
    done
}

################################################################################
# Plugin functions
################################################################################
 
#
# Registers a plugin for the application.
#
# This should the first thing a plugin (.shp) script does.
#
# @param name
#   The name of the plugin.
#
# @param switch
#   The command-line switch used to access the plugin, using --switch.
#
# @param description
#   A description of the plugin. This is displayed when the --help switch
#   is used.
#
# @return
#   Does not return a value
#
# Example
# @code
#   registerPlugin 'MyPlugin' 'my' 'An example plugin'
#
registerPlugin()
{
    sPluginName=$1
    sPluginSwitch=$2
    sPluginDescription=$3

    debug "Registering $sPluginName"

    array_push "pluginNames" "$sPluginName"
    index=$(array_find "pluginNames" "$sPluginName")
    array_set "pluginDescriptions" "$index" "$sPluginDescription"
    array_set "pluginSwitches" "$index" "$sPluginSwitch"
}

#
# Registers a command for the current plugin.
#
# This must be called after registerPlugin.
#
# @param name
#   Name of the command.
#
# @param switch
#   The command-line switch used to access the command, using --switch.
#
# @param description
#   A description of the command. This is displayed when the --help switch
#   is used for a plugin.
#
# @return
#   Does not return a value.
#
# Example:
# @code
#   registerCommand 'MyCommand' 'command' 'An example command'
#
registerCommand() 
{
    sCommandName=$1
    sCommandSwitch=$2
    sCommandDescription=$3

    debug "Registering command $sCommandName"

    array_add "${sPluginName}${sCommandSwitch}" "$sCommandName" "$sPluginName" "$sCommandDescription"
    array_push "${sPluginName}Commands" "${sCommandSwitch}"

    # Awesome OOP hack?
    eval ${sPluginName}::${sCommandName}\(\) \{ ${sPluginName}\; ${sCommandName}\; Main \$@\; \}
    eval _::${sCommandName}\(\) \{ ${sCommandName}\; Main \$@\; \}
    eval ${sPluginName}::version\(\) \{ ${sPluginName}\; Version \$@\; \}
}

################################################################################
# Helper functions
################################################################################

registerHelper()
{
    sHelperName=$1
    sHelperDescription=$2

    debug "Registering helper $sHelperName"

    array_push "helperNames" "$sHelperName"
    index=$(array_find "helperNames" "$sHelperName")
    array_set "helperDescriptions" "$index" "$sHelperDescription"

    eval ${sHelperName}::version\(\) \{ ${sHelperName}\; Version \$@\; \}
}

registerHelperCommand() 
{
    sHelperCommandName=$1
    sHelperCommandDescription=$2

    debug "Registering helper command $sHelperCommandName"

    # Awesome OOP hack?
    eval ${sHelperName}::${sHelperCommandName}\(\) \{ ${sHelperName}\; ${sHelperCommandName}\; Main \$@\; \}
    eval _::${sHelperCommandName}\(\) \{ ${sHelperCommandName}\; Main \$@\; \}

}

################################################################################
# Application functions
################################################################################

### Helper functions ###

# TODO show versions for plugins

# 
# Displays the BPA version and exits.
#
# @return
#   Does not return a value.
#
showVersion()
{
    echo "BPA version: $sBPAVersion"
	if [ -n "$sScriptVersion" ]; then
		echo "$sAppName script version: $sScriptVersion"
	fi
	if [ -n "$sSelectedPlugin" ]; then
		echo "$sSelectedPlugin plugin version: "$("${sSelectedPlugin}::version")
	fi
    exit 0
}

#
# Generates and displays the usage text for the applications.
#
# @return
#   Does not return a value.
#
usage() 
{
    # @todo this should probably all be done with printfs for better format

    fmtShort="    %-10s    %-20s\n"
    fmtLong="    %-10s    %-10s    %-30s\n"

    echo "$sAppName Usage"
    echo "Built-in options:"
    printf "$fmtShort" "--help" "This help"
    printf "$fmtShort" "--debug" "Enable runtime debugging"
    printf "$fmtShort" "--version" "Display version"
    echo ""

    if [ -z "$sSelectedPlugin" ]; then
        echo "Available plugins:"
        # TODO Need an array_each that returns index instead of values
        for sName in $(array_each "pluginNames"); do
            iIndex=$(array_find "pluginNames" "$sName")
            sSwitch=$(array_get "pluginSwitches" "$iIndex")
            sDesc=$(array_get "pluginDescriptions" "$iIndex")
            printf "$fmtLong" "--${sSwitch}" "$sName" "$sDesc"
        done
    else
        if [ -z "$sSelectedCommand" ]; then
            $sSelectedPlugin
            Help
            echo ""
            echo "Available commands for $sSelectedPlugin:"
            for sCommandSwitch in $(array_each "${sSelectedPlugin}Commands"); do
                sCommandName=$(array_get "${sSelectedPlugin}${sCommandSwitch}" 0)
                sCommandDesc=$(array_get "${sSelectedPlugin}${sCommandSwitch}" 2)
                printf "$fmtLong" "--${sCommandSwitch}" "$sCommandName" "$sCommandDesc"
            done
        else
            $sSelectedPlugin
            # Reset the Help function as it may have been defined in the plugin
            Help() { echo $sHelpDefault; }
            $sSelectedCommand
            Help
        fi
    fi

    exit 0
}

#
# Parses the arguments given to it to determine which plugin
# and command to run.
#
# @param args
#   The array of arguments given to the application script.
#
# @return
#   Does not return a value.
#
parse() 
{
    [ $# -eq 0 ] && usage

    while [ $# -gt 0 ]
    do
        case "$1" in
            -h)        usage;;
            --help)    usage;;
            --version) showVersion;;
            --debug)   doDebug=1; shift; continue;;
            --trace)   set -x; shift; continue;;
            --)        shift; break;;
        esac
    
        if [ -n "$sSelectedCommand" ]; then
            break
        fi

        switch=${1:2}
        if [ -z "$sSelectedPlugin" ]; then
            iPluginIndex=$(array_find pluginSwitches "$switch")
            sSelectedPlugin=$(array_get pluginNames "$iPluginIndex")
            sSelectedPluginSwitch=$switch
        else
            sSelectedCommand=$(array_get "${sSelectedPlugin}${switch}" 0)
        fi

        shift
    done

    aCmdArgs=$@
}

### Main application functions ###

#
# Registers a new application.
#
# This must be the first thing called inside the application script.
#
# @param name
#   Name of the application.
#
# @param debug
#   (optional) If set to '1' debugging is enabled for the application. Defaults
#   to '0'.
#
# @param plugins
#   (optional) Sets the plugins directory for the application. Defaults to
#   './plugins' inside the directory of the application script.
#
# @return
#   Does not return a value
#
# Example:
# @code
#   new 'MyApp' 1 '/var/lib/plugins/myapp'
# 
new()
{
    sAppName="$1"
    bDebug=${2:-"0"}
    sAppPluginsDir=${3:-"./plugins"}

    doDebug=$bDebug

    debug "New application $sAppName"
    for plugin in $(ls ${sAppPluginsDir}/*.shp); do
        debug "Sourcing $plugin"
        source "$plugin"
    done
}

version()
{
	sScriptVersion="$1"
}
#
# Sets the arguments that were passed to the application script.
#
# It must be the function to run inside the application script.
#
# @param args
#   Array of args passed to the application.
#
# @return
#   Does not return a value.
#
# Example:
# @code
#   args $@
#
args()
{
    debug "Setting command line argument"
    aAppArgs=$@
}

#
# Runs the actual application.
#
# @return
#   Does not return a value.
#
# Example:
# @code
#   run
#
run()
{
    debug "Running $sAppName"
    parse $aAppArgs
    debug "Plugin: $sSelectedPlugin"
    debug "Command: $sSelectedCommand"
    $sSelectedPlugin
    $sSelectedCommand
    Main $aCmdArgs
}
